package pl.edu.wsinf.lukaszwalczak.emailclient.service;

import com.sun.mail.imap.IMAPMessage;
import com.sun.mail.smtp.SMTPTransport;
import java.awt.Desktop;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.stage.FileChooser;
import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.activation.FileDataSource;
import javax.inject.Inject;
import javax.mail.Address;
import javax.mail.AuthenticationFailedException;
import javax.mail.Authenticator;
import javax.mail.BodyPart;
import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.NoSuchProviderException;
import javax.mail.Part;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.Store;
import javax.mail.Transport;
import javax.mail.internet.AddressException;
import javax.mail.internet.ContentType;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import javax.mail.internet.MimeUtility;
import javax.mail.internet.ParseException;
import javax.mail.search.ComparisonTerm;
import javax.mail.search.ReceivedDateTerm;
import javax.mail.search.SentDateTerm;
import org.apache.commons.io.IOUtils;
import org.joda.time.DateTime;
import org.springframework.stereotype.Service;
import org.springframework.util.FileCopyUtils;
import pl.edu.wsinf.lukaszwalczak.emailclient.pojo.Account;
import pl.edu.wsinf.lukaszwalczak.emailclient.pojo.Account.IncomingProtocol;
import pl.edu.wsinf.lukaszwalczak.emailclient.pojo.Account.ServerAuth;
import pl.edu.wsinf.lukaszwalczak.emailclient.pojo.Attachment;
import pl.edu.wsinf.lukaszwalczak.emailclient.pojo.Content;
import pl.edu.wsinf.lukaszwalczak.emailclient.pojo.EmailAddress;
import pl.edu.wsinf.lukaszwalczak.emailclient.pojo.Mail;
import pl.edu.wsinf.lukaszwalczak.emailclient.repository.AttachmentReposiotry;
import pl.edu.wsinf.lukaszwalczak.emailclient.repository.ContentRepository;
import pl.edu.wsinf.lukaszwalczak.emailclient.repository.FolderRepository;
import pl.edu.wsinf.lukaszwalczak.emailclient.repository.MailRepository;

@Service
public class MailService {

    private static final Logger LOG = Logger.getLogger(MailService.class.getName());
    @Inject
    private MailRepository mailRepository;
    @Inject
    private ContentRepository contentRepository;
    @Inject
    private FolderRepository folderRepository;
    @Inject
    private AttachmentReposiotry attachmentReposiotry;
    @Inject
    private AccountService accountService;
    static private int SUBJECT_MAX_LENGTH = 255;
    static private int ADDRESS_MAX_LENGTH = 255;
    static private int PERSONAL_MAX_LENGTH = 255;
    static private int MESSAGE_ID_MAX_LENGTH = 255;
    static private int CONTENT_MAX_LENGTH = Integer.MAX_VALUE;

    public Mail save(Account account, Map<String, Object> values) {

        Account a;
        if (account != null) {
            a = account;
        } else {
            a = accountService.getFirstAccount();
        }

        Mail mail = new Mail();
        mail.setAccount(a);
        mail.setFrom(new EmailAddress(a.getEmailAddress(), null));
        mail.setTo((Set) values.get("to"));
        mail.setSubject((String) values.get("subject"));
        mail.setSentDate(new DateTime());

        if (values.containsKey("cc")) {
            mail.setCc((Set) values.get("cc"));
        }

        Content content = new Content();
        content.setHtml((String) values.get("content"));
        content = contentRepository.save(content);
        mail.setContent(content);

        mail.setFolder(accountService.getDraftsFolder(a).getName());

        mail = mailRepository.save(mail);

        if (values.containsKey("attachments")) {
            List<File> files = (List<File>) values.get("attachments");
            Set<Attachment> attachments = new HashSet<>();
            for (File sourceFile : files) {
                Attachment attachment = createAttachment(mail, sourceFile.getName(), sourceFile);
                if (attachment != null) {
                    attachments.add(attachment);
                }
            }
            mail.setAttachments(attachments);
        }

        return mail;
    }

    public void send(Mail mail) {
        try {
            Account account = mail.getAccount();

            Properties props = new java.util.Properties();
            configureAuthentication(account, props);

            props.put("mail.transport.protocol", "smtp");
            props.put("mail.smtp.host", account.getOutgoingServer());
            props.put("mail.smtp.connectiontimeout", "300000");
            props.put("mail.smtp.timeout", "300000");

            if (account.getOutgoingServerPort() != null) {
                props.put("mail.smtp.port", account.getOutgoingServerPort().toString());
            }

            Session session = Session.getInstance(props, new AuthenticatorImpl(account.getUserName(), account.getPassword()));

            ArrayList<Address> to = new ArrayList<>(mail.getTo().size()),
                    cc = new ArrayList<>(mail.getCc().size());

            for (String adr : mail.getTo()) {
                try {
                    if (adr != null && !adr.isEmpty()) {
                        to.add(new InternetAddress(adr));
                    }
                } catch (AddressException ex) {
                    LOG.log(Level.SEVERE, null, ex);
                }
            }

            for (String adr : mail.getCc()) {
                try {
                    if (adr != null && !adr.isEmpty()) {
                        cc.add(new InternetAddress(adr));
                    }
                } catch (AddressException ex) {
                    LOG.log(Level.SEVERE, null, ex);
                }
            }

            MimeMessage msg = new MimeMessage(session);
            msg.setFrom(new InternetAddress(account.getEmailAddress()));
            msg.setRecipients(Message.RecipientType.TO, to.toArray(new Address[]{}));
            msg.setRecipients(Message.RecipientType.CC, cc.toArray(new Address[]{}));
            msg.setSubject(mail.getSubject());
            msg.setSentDate(new Date());

            Multipart multipart = new MimeMultipart();
            BodyPart contentBodyPart = new MimeBodyPart();
            Multipart alternativeMultipart = new MimeMultipart("alternative");

            Content content = mail.getContent();
            BodyPart htmlBodyPart = new MimeBodyPart();
            htmlBodyPart.setContent(content.getHtml(), "text/html; charset=utf-8");
            alternativeMultipart.addBodyPart(htmlBodyPart);
            contentBodyPart.setContent(alternativeMultipart);
            multipart.addBodyPart(contentBodyPart);

            if (mail.getAttachments() != null) {
                for (Attachment attachment : mail.getAttachments()) {
                    try {
                        File file = new File(attachment.getAbsolutePath());
                        if (file.exists()) {
                            BodyPart attachmentBodyPart = new MimeBodyPart();
                            DataSource source = new FileDataSource(file);
                            attachmentBodyPart.setDataHandler(new DataHandler(source));
                            attachmentBodyPart.setFileName(MimeUtility.encodeText(attachment.getFileName(), "UTF-8", null));
                            attachmentBodyPart.setDisposition(Part.ATTACHMENT);
                            multipart.addBodyPart(attachmentBodyPart);
                        }
                    } catch (UnsupportedEncodingException exception) {
                        LOG.log(Level.SEVERE, null, exception);
                    }
                }
            }

            msg.setContent(multipart);

            Transport.send(msg);

            mail.setMessageID(checkLength(msg.getMessageID(), MESSAGE_ID_MAX_LENGTH));
            mail.setFolder(accountService.getSentFolder(mail.getAccount()).getName());
            mailRepository.save(mail);

        } catch (MessagingException ex) {
            LOG.log(Level.SEVERE, null, ex);
        }
    }

    public void download(Account account) {
        try {
            Store store = connectToStore(account);

            Folder[] folders = store.getDefaultFolder().list("*");
            List<Mail> mails;
            mails = new ArrayList<>();

            for (Folder folder : folders) {

                try {

                    if ((folder.getType() & Folder.HOLDS_MESSAGES) == 0) {
                        // Jesli folder nie moze trzymac wiadomosci to go pomijamy
                        continue;
                    }

                    if (folder.getName() != null) {
                        if (folderRepository.findByAccountAndName(account, folder.getName()) == null) {
                            folderRepository.save(new pl.edu.wsinf.lukaszwalczak.emailclient.pojo.Folder(account, folder.getName()));
                        }
                    } else {
                        continue;
                    }

                    Message[] messages = getMessages(account, folder, Folder.READ_ONLY);

                    for (Message msg : messages) {
                        Mail mail = new Mail();

                        MimeMessage m = (MimeMessage) msg;
                        if (m instanceof IMAPMessage) {
                            ((IMAPMessage) m).setPeek(true);
                        }

                        String messageID = m.getMessageID();
                        mail.setMessageID(checkLength(messageID, MESSAGE_ID_MAX_LENGTH));
                        mail.setAccount(account);

                        // Sprawdzamy czy dany email istnieje juz w bazie
                        if (mailRepository.findByAccountAndMessageID(account, mail.getMessageID()) != null) {
                            continue;
                        }

                        Date sentDate = m.getSentDate();
                        mail.setSentDate(sentDate != null ? new DateTime(m.getSentDate()) : new DateTime());
                        mail.setFolder(folder.getName());

                        String subject = m.getSubject();
                        if (isAscii(subject) == false && m.getHeader("Subject") != null && m.getHeader("Subject").length > 0) {
                            String subjectFromHeader = m.getHeader("Subject")[0];
                            if (subjectFromHeader.indexOf("=?") == -1) {
                                try {
                                    subject = new String(subjectFromHeader.getBytes(), "UTF8");
                                } catch (UnsupportedEncodingException ex) {
                                    LOG.log(Level.SEVERE, null, ex);
                                }
                            }
                        }

                        mail.setSubject(checkLength(subject, SUBJECT_MAX_LENGTH));

                        System.out.println("DOWNLOADING:");
                        System.out.println("Account: " + account.getId());
                        System.out.println(mail.getSubject());
                        System.out.println(mail.getSentDate());

                        try {
                            Address[] from = m.getFrom();
                            if (from.length > 0) {
                                InternetAddress senderAddress = ((InternetAddress) m.getFrom()[0]);
                                mail.setFrom(new EmailAddress(checkLength(senderAddress.getAddress(), ADDRESS_MAX_LENGTH),
                                        checkLength(senderAddress.getPersonal(), PERSONAL_MAX_LENGTH)));
                            }
                        } catch (AddressException ex) {
                            //The exception thrown due to an error in parsing RFC822 or MIME headers
                        }

                        //See http://www.oracle.com/technetwork/java/faq-135477.html#imapserverbug
                        MimeMessage fetchedMsg = new MimeMessage(m);

                        try {
                            mail.setTo(prepareAddressSet(fetchedMsg.getRecipients(Message.RecipientType.TO)));
                            mail.setCc(prepareAddressSet(fetchedMsg.getRecipients(Message.RecipientType.CC)));
                            mail.setBcc(prepareAddressSet(fetchedMsg.getRecipients(Message.RecipientType.BCC)));
                        } catch (AddressException ex) {
                            //The exception thrown due to an error in parsing RFC822 or MIME headers
                        }

                        // Zapisujemy mail do bazy
                        mailRepository.save(mail);

                        try {
                            retrieveContent(fetchedMsg, mail);
                        } catch (IOException ex) {
                        }

                        if (mail.getContent() != null) {
                            contentRepository.save(mail.getContent());
                            mailRepository.save(mail);
                        }

                        mails.add(mail);
                    }
                } catch (MessagingException ex) {
                    LOG.log(Level.SEVERE, null, ex);
                }
            }
        } catch (NoSuchProviderException ex) {
            LOG.log(Level.SEVERE, null, ex);
        } catch (MessagingException ex) {
            LOG.log(Level.SEVERE, null, ex);
        }
    }

    private Message[] getMessages(Account account, Folder folder, int mode) throws MessagingException {
        Message[] messages = new Message[0];
        try {
            folder.open(mode);
            DateTime downloadDate;
            // Jesli Account ma wypelnione downloadDate to wtedy pobiera
            // wiadomosci nadeslane po tej dacie. W innym wypadku na miesiac wstecz.
            if (account.getDownloadDate() != null) {
                downloadDate = account.getDownloadDate();
            } else {
                downloadDate = DateTime.now().minusWeeks(1);
            }
            if (account.getIncomingServerProtocol().equals(IncomingProtocol.IMAP)) {
                ReceivedDateTerm dateTerm = new ReceivedDateTerm(ComparisonTerm.GE, downloadDate.toDate());
                messages = folder.search(dateTerm);
            } else {
                SentDateTerm dateTerm = new SentDateTerm(ComparisonTerm.GE, downloadDate.toDate());
                messages = folder.search(dateTerm);
            }
        } catch (MessagingException ex) {
            closeConnections(account, null, folder, false);
            LOG.log(Level.SEVERE, null, ex);
        }
        return messages;
    }

    private void retrieveContent(Part part, Mail mail) throws IOException, MessagingException {
        if (Part.ATTACHMENT.equalsIgnoreCase(part.getDisposition()) || part.getFileName() != null) {
            retrieveAttachment(mail, part);
        } else {
            Object content = part.getContent();
            if (content instanceof String) {
                retrieveText(mail, content, part);
            } else if (content instanceof MimeMultipart) {
                MimeMultipart multipart = (MimeMultipart) content;
                int bodyParts = multipart.getCount();
                for (int i = 0; i < bodyParts; i++) {
                    BodyPart bodyPart = multipart.getBodyPart(i);
                    try {
                        retrieveContent(bodyPart, mail);
                    } catch (IOException ex) {
                    }
                }
            }
        }
    }

    private void retrieveAttachment(Mail mail, Part bodyPart) throws MessagingException {
        try {
            createAttachment(mail, bodyPart.getFileName(), bodyPart.getInputStream());
        } catch (IOException exception) {
            LOG.log(Level.SEVERE, "Retrieve attchment error", exception);
        }
    }

    private void retrieveText(Mail mail, Object content, Part part) throws MessagingException {
        String text;
        ContentType contentType = null;
        String charset = null;
        try {
            contentType = new ContentType(part.getContentType());
            charset = contentType.getParameter("charset");
        } catch (ParseException pex) {
        }
        if (charset == null) {
            try {
                String[] ctHeader = part.getHeader("Content-Type");
                if (ctHeader != null && ctHeader.length > 0) {
                    charset = new ContentType(ctHeader[0]).getParameter("charset");
                }
            } catch (ParseException pex) {
            }
            try {
                text = new String(IOUtils.toByteArray(part.getInputStream()), charset != null ? MimeUtility.javaCharset(charset) : "UTF8");
            } catch (IOException | MessagingException ex) {
                text = content.toString();
            }
        } else {
            text = content.toString();
        }
        try {
            text = checkLength(text, CONTENT_MAX_LENGTH);
        } catch (Throwable ex) {
            text = "";
        }
        if (mail.getContent() == null) {
            mail.setContent(new Content());
        }
        if (contentType != null && contentType.getBaseType().equalsIgnoreCase("text/html")) {
            if (mail.getContent().getHtml() != null) {
                text = checkLength(mail.getContent().getHtml() + text, CONTENT_MAX_LENGTH);
            }
            mail.getContent().setHtml(text);
        } else {
            if (mail.getContent().getPlain() != null) {
                text = checkLength(mail.getContent().getPlain() + text, CONTENT_MAX_LENGTH);
            }
            mail.getContent().setPlain(text);
        }
    }

    public Map<String, Object> checkConnection(Account account) {
        Map<String, Object> result = new HashMap<>();
        Store store = null;
        try {
            store = connectToStore(account);
        } catch (AuthenticationFailedException ex) {
            result.put("success", false);
            result.put("message", ex.getMessage());
            result.put("errorCode", "authenticationFailed");
        } catch (MessagingException ex) {
            result.put("success", false);
            result.put("message", ex.getMessage());
            result.put("server", "INCOMING");
        }
        closeConnections(account, store, null, false);
        if (result.containsKey("success") == false) {
            Properties props = new java.util.Properties();
            props.put("mail.smtp.connectiontimeout", "60000");
            props.put("mail.smtp.timeout", "60000");
            configureAuthentication(account, props);

            Session session = Session.getInstance(props);
            SMTPTransport t = new SMTPTransport(session, null);
            try {
                t.connect(account.getOutgoingServer(), account.getOutgoingServerPort(), account.getUserName(), account.getPassword());
            } catch (MessagingException ex) {
                result.put("success", false);
                result.put("message", ex.getMessage());
                result.put("server", "OUTGOING");
            }
            if (t.isConnected()) {
                try {
                    t.close();
                } catch (MessagingException ex) {
                }
            }
        }
        if (result.containsKey("success") == false) {
            result.put("success", true);
        }
        return result;
    }

    private Store connectToStore(Account account) throws NoSuchProviderException, MessagingException {
        Properties props = new java.util.Properties();
        props.put("mail.pop3.connectiontimeout", "60000");
        props.put("mail.pop3.timeout", "60000");
        props.put("mail.imap.connectiontimeout", "60000");
        props.put("mail.imap.timeout", "60000");
        props.put("mail.imap.partialfetch", false);
        configureAuthentication(account, props);
        Session session = Session.getInstance(props);
        session.setDebug(true);
        Store store = session.getStore(account.getIncomingServerProtocol().
                name().toLowerCase());
        if (account.getIncomingServerPort() != null) {
            store.connect(account.getIncomingServer(), account.getIncomingServerPort(), account.getUserName(), account.getPassword());
        } else {
            store.connect(account.getIncomingServer(), account.getUserName(), account.getPassword());
        }
        return store;
    }

    private void closeConnections(Account ma, Store store, Folder folder, boolean expunge) {
        try {
            if (folder != null && folder.isOpen()) {
                folder.close(expunge);
            }
        } catch (Exception ex) {
        }
        try {
            if (store != null && store.isConnected()) {
                store.close();
            }
        } catch (Exception ex) {
        }
    }

    private void configureAuthentication(Account account, Properties props) {
        if (!account.getIncomingServerAuth().equals(ServerAuth.NO_AUTH)) {
            if (account.getIncomingServerProtocol().equals(IncomingProtocol.POP3)) {
                props.put("mail.pop3.ssl.trust", "*");
                if (account.getIncomingServerAuth().equals(ServerAuth.SSLTLS)) {
                    props.put("mail.pop3.ssl.enable", "true");
                } else if (account.getIncomingServerAuth().equals(ServerAuth.STARTTLS)) {
                    props.put("mail.pop3.starttls.enable", "true");
                }
            } else {
                props.put("mail.imap.ssl.trust", "*");
                if (account.getIncomingServerAuth().equals(ServerAuth.SSLTLS)) {
                    props.put("mail.imap.ssl.enable", "true");
                } else if (account.getIncomingServerAuth().equals(ServerAuth.STARTTLS)) {
                    props.put("mail.imap.starttls.enable", "true");
                }
            }
        }
        if (!account.getOutgoingServerAuth().equals(ServerAuth.NO_AUTH)) {
            props.put("mail.smtp.auth", "true");
            props.put("mail.smtp.ssl.trust", "*");
            if (account.getOutgoingServerAuth().equals(ServerAuth.STARTTLS)) {
                props.put("mail.smtp.starttls.enable", "true");
            } else {
                props.put("mail.smtp.ssl.enable", "true");
            }
        }
    }

    private String checkLength(String content, int maxLength) {
        if (content != null && content.length() > maxLength) {
            content = content.substring(0, maxLength);
        }
        return content;
    }

    public boolean isAscii(String s) {
        if (s != null) {
            for (int i = 0; i < s.length(); i++) {
                int b = (int) s.charAt(i);
                if (b >= 0177 || (b < 040 && b != '\r' && b != '\n' && b != '\t')) {
                    return false;
                }
            }
        }
        return true;
    }

    private Set<String> prepareAddressSet(Address[] addresses) {
        if (addresses != null) {
            Set<String> result = new HashSet<>(addresses.length);
            for (Address a : addresses) {
                InternetAddress ia = (InternetAddress) a;
                String address = checkLength(ia.getAddress(), 100);
                if (address != null) {
                    result.add(address.toLowerCase());
                }
            }
            return result;
        } else {
            return null;
        }
    }

    class AuthenticatorImpl extends Authenticator {

        private String username;
        private String password;

        public AuthenticatorImpl(String username, String password) {
            this.username = username;
            this.password = password;
        }

        @Override
        public PasswordAuthentication getPasswordAuthentication() {
            return new PasswordAuthentication(username, password);
        }
    }

    public void handleAttachmentOpen(Attachment attachment, boolean forceSave) {
        try {
            File file = new File(attachment.getAbsolutePath());
            if (file.exists()) {
                if (Desktop.isDesktopSupported() && !forceSave) {
                    Desktop.getDesktop().open(file);
                } else {
                    FileChooser fileChooser = new FileChooser();
                    File destFile = fileChooser.showSaveDialog(null);
                    if (destFile != null) {
                        FileCopyUtils.copy(file, destFile);
                    }
                }
            }
        } catch (IOException ex) {
            LOG.log(Level.SEVERE, null, ex);
        }
    }

    private File getAttachmentFile(String generatedName) {
        String os = System.getProperty("os.name").toLowerCase();
        String appDataPath = null;
        if (os != null) {
            if (os.contains("win")) {
                appDataPath = System.getenv("APPDATA");
                if (appDataPath != null) {
                    appDataPath += "\\EmailClient\\attachments";
                }
            } else if (os.contains("linux")) {
                appDataPath = System.getProperty("user.home");
                if (appDataPath != null) {
                    appDataPath += "/.EmailClient/attachments";
                }
            }
        }
        if (appDataPath != null) {
            // Tworzenie katalogu ktory bedzie przechowywal zalaczniki
            File folder = new File(appDataPath);
            if (!folder.exists()) {
                folder.mkdirs();
            }
            return new File(folder, generatedName);
        }
        return null;
    }

    private synchronized Attachment createAttachment(Mail mail, String fileName, Object source) {
        try {
            String destFileName = String.valueOf(new Date().getTime());
            File destFile = getAttachmentFile(destFileName);
            if (destFile != null) {
                if (destFile.exists() == false) {
                    destFile.createNewFile();
                }
                if (source instanceof File) {
                    FileCopyUtils.copy((File) source, destFile);
                } else if (source instanceof InputStream) {
                    FileCopyUtils.copy((InputStream) source, new FileOutputStream(destFile));
                }
                Attachment attachment = new Attachment();
                attachment.setGeneratedName(destFileName);
                attachment.setFileName(fileName);
                attachment.setAbsolutePath(destFile.getAbsolutePath());
                attachment.setMail(mail);
                return attachmentReposiotry.save(attachment);
            }
        } catch (IOException exception) {
            LOG.log(Level.SEVERE, "Attchments copy error", exception);
        }
        return null;
    }
}
